diff --git a/apps/cli/e2e/server/basic.e2e-spec.ts b/apps/cli/e2e/server/basic.e2e-spec.ts index 46eb0557..e05ad713 100644 --- a/apps/cli/e2e/server/basic.e2e-spec.ts +++ b/apps/cli/e2e/server/basic.e2e-spec.ts @@ -16,7 +16,10 @@ describe('Server - Basic', () => { beforeAll(async () => { mockedBackend = jestMockBackend(); - server = new ADCServer({ listen: new URL('http://127.0.1:3000') }); + server = new ADCServer({ + listen: new URL('http://127.0.1:3000'), + listenStatus: 3001, + }); }); it('test mocked load backend', async () => { @@ -85,7 +88,7 @@ describe('Server - Basic', () => { it('test server listen', async () => { const url = new URL(`http://127.0.0.1:48562`); - const server = new ADCServer({ listen: url }); + const server = new ADCServer({ listen: url, listenStatus: 3001 }); await server.start(); const { status, data } = await axios.put(`${url.origin}/sync`, { @@ -108,6 +111,7 @@ describe('Server - Basic', () => { const url = new URL(`https://127.0.0.1:48562`); const server = new ADCServer({ listen: url, + listenStatus: 3001, tlsCert: readFileSync( join(__dirname, '../assets/tls/server.cer'), 'utf-8', @@ -145,6 +149,7 @@ describe('Server - Basic', () => { readFileSync(join(__dirname, '../assets/tls/', fileName), 'utf-8'); const server = new ADCServer({ listen: url, + listenStatus: 3001, tlsCert: readCert('server.cer'), tlsKey: readCert('server.key'), tlsCACert: readCert('ca.cer'), @@ -198,7 +203,7 @@ describe('Server - Basic', () => { it('test server listen (with UDS)', async () => { const url = new URL(`unix:///tmp/adc-test.sock`); - const server = new ADCServer({ listen: url }); + const server = new ADCServer({ listen: url, listenStatus: 3001 }); await server.start(); const { status, data } = await new Promise<{ @@ -250,4 +255,36 @@ describe('Server - Basic', () => { await server.stop(); }); + + it('test status listen', async () => { + const server = new ADCServer({ + listen: new URL(`http://127.0.0.1:3000`), + listenStatus: 3001, + }); + await server.start(); + + const { status, data } = await axios.get( + `http://127.0.0.1:3001/healthz/ready`, + ); + expect(status).toEqual(200); + expect(data).toEqual('OK'); + + await server.stop(); + }); + + it('test status listen (custom port)', async () => { + const server = new ADCServer({ + listen: new URL(`http://127.0.0.1:3000`), + listenStatus: 30001, + }); + await server.start(); + + const { status, data } = await axios.get( + `http://127.0.0.1:30001/healthz/ready`, + ); + expect(status).toEqual(200); + expect(data).toEqual('OK'); + + await server.stop(); + }); }); diff --git a/apps/cli/src/command/ingress-server.command.ts b/apps/cli/src/command/ingress-server.command.ts index aa3ee95e..92faf46b 100644 --- a/apps/cli/src/command/ingress-server.command.ts +++ b/apps/cli/src/command/ingress-server.command.ts @@ -7,6 +7,7 @@ import { BaseCommand, BaseOptions, processCertificateFile } from './helper'; type IngressServerOptions = { listen?: URL; + listenStatus?: number; caCertFile?: string; tlsCertFile?: string; tlsKeyFile?: string; @@ -16,7 +17,7 @@ export const IngressServerCommand = new BaseCommand( 'server', ) .option( - '--listen ', + '--listen ', 'listen address of ADC server, the format is scheme://host:port', (val) => { try { @@ -27,6 +28,19 @@ export const IngressServerCommand = new BaseCommand( }, new URL('http://127.0.0.1:3000'), ) + .option( + '--listen-status ', + 'status listen port', + (val) => { + const port = parseInt(val, 10); + if (!port || isNaN(port) || port < 1 || port > 65535) + throw new commander.InvalidArgumentError( + 'The status listen port must be a number between 1 and 65535', + ); + return port; + }, + 3001, + ) .addOption( new Option( '--ca-cert-file ', @@ -60,28 +74,31 @@ export const IngressServerCommand = new BaseCommand( ), ), ) - .handle(async ({ listen, tlsCertFile, tlsKeyFile, caCertFile }) => { - if (listen.protocol === 'https:' && (!tlsCertFile || !tlsKeyFile)) { - console.error( - chalk.red( - 'Error: When using HTTPS, both --tls-cert-file and --tls-key-file must be provided', - ), + .handle( + async ({ listen, listenStatus, tlsCertFile, tlsKeyFile, caCertFile }) => { + if (listen.protocol === 'https:' && (!tlsCertFile || !tlsKeyFile)) { + console.error( + chalk.red( + 'Error: When using HTTPS, both --tls-cert-file and --tls-key-file must be provided', + ), + ); + return; + } + const server = new ADCServer({ + listen, + listenStatus, + tlsCert: tlsCertFile ? readFileSync(tlsCertFile, 'utf-8') : undefined, + tlsKey: tlsKeyFile ? readFileSync(tlsKeyFile, 'utf-8') : undefined, + tlsCACert: caCertFile ? readFileSync(caCertFile, 'utf-8') : undefined, + }); + await server.start(); + console.log( + `ADC server is running on: ${listen.protocol === 'unix:' ? listen.pathname : listen.origin}`, ); - return; - } - - const server = new ADCServer({ - listen, - tlsCert: tlsCertFile ? readFileSync(tlsCertFile, 'utf-8') : undefined, - tlsKey: tlsKeyFile ? readFileSync(tlsKeyFile, 'utf-8') : undefined, - tlsCACert: caCertFile ? readFileSync(caCertFile, 'utf-8') : undefined, - }); - await server.start(); - console.log( - `ADC server is running on: ${listen.protocol === 'unix:' ? listen.pathname : listen.origin}`, - ); - process.on('SIGINT', () => { - console.log('Stopping, see you next time!'); - server.stop(); - }); - }); + process.on('SIGINT', () => { + console.log('Stopping, see you next time!'); + server.stop(); + process.exit(0); + }); + }, + ); diff --git a/apps/cli/src/server/index.ts b/apps/cli/src/server/index.ts index 6603035e..bf3f46d1 100644 --- a/apps/cli/src/server/index.ts +++ b/apps/cli/src/server/index.ts @@ -8,20 +8,28 @@ import { syncHandler } from './sync'; interface ADCServerOptions { listen: URL; + listenStatus: number; tlsCert?: string; tlsKey?: string; tlsCACert?: string; } export class ADCServer { private readonly express: Express; + private readonly expressStatus: Express; private server?: http.Server | https.Server; + private serverStatus?: http.Server; constructor(private readonly opts: ADCServerOptions) { this.express = express(); - this.express.disable('x-powered-by'); - this.express.disable('etag'); + this.expressStatus = express(); + [this.express, this.expressStatus].forEach( + (app) => (app.disable('x-powered-by'), app.disable('etag')), + ); this.express.use(express.json({ limit: '100mb' })); this.express.put('/sync', syncHandler); + this.expressStatus.get('/healthz/ready', (_, res) => + res.status(200).send('OK'), + ); } public async start() { @@ -48,30 +56,46 @@ export class ADCServer { this.server = http.createServer(this.express); break; } - return new Promise((resolve) => { - const listen = this.opts.listen; - if (listen.protocol === 'unix:') { - if (fs.existsSync(listen.pathname)) fs.unlinkSync(listen.pathname); - this.server.listen(listen.pathname, () => { - fs.chmodSync(listen.pathname, 0o660); - resolve(); - }); - } else { - this.server.listen(parseInt(listen.port), listen.hostname, () => - resolve(), - ); - } - }); + return Promise.all([ + new Promise((resolve) => { + const listen = this.opts.listen; + if (listen.protocol === 'unix:') { + if (fs.existsSync(listen.pathname)) fs.unlinkSync(listen.pathname); + this.server.listen(listen.pathname, () => { + fs.chmodSync(listen.pathname, 0o660); + resolve(); + }); + } else { + this.serverStatus = this.server.listen( + parseInt(listen.port), + listen.hostname, + () => resolve(), + ); + } + }), + new Promise((resolve) => { + this.expressStatus.listen(this.opts.listenStatus, () => resolve()); + }), + ]); } public async stop() { - return new Promise((resolve) => { - if (this.server) { - this.server.close(() => resolve()); - } else { - resolve(); - } - }); + return Promise.all([ + new Promise((resolve) => { + if (this.server) { + this.server.close(() => resolve()); + } else { + resolve(); + } + }), + new Promise((resolve) => { + if (this.serverStatus) { + this.serverStatus.close(() => resolve()); + } else { + resolve(); + } + }), + ]); } public TEST_ONLY_getExpress() {