diff --git a/Dockerfile b/Dockerfile index 8a11772..35eee81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,4 +37,4 @@ EXPOSE 8080 # use tini as init process since Node.js isn't designed to be run as PID 1 ENTRYPOINT ["/sbin/tini", "--"] -CMD ["node", "--disable-proto=delete", "dist/server.js"] +CMD ["node", "--disable-proto=delete", "dist/main.js"] diff --git a/package.json b/package.json index ead3a98..3566dc3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "npm run build --workspaces && node -e \"fs.rmSync('./dist',{force:true,recursive:true})\" && tsc", "lint": "npm run lint --workspaces && tsc --noEmit -p tsconfig.lint.json && eslint --ignore-path .gitignore src", "test": "npm run test --workspaces --if-present && mocha --require tsx --recursive \"test/**/*.ts\"", - "start": "node --disable-proto=delete dist/server.js" + "start": "node --disable-proto=delete dist/main.js" }, "repository": { "type": "git", diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0f7b2be --- /dev/null +++ b/src/main.ts @@ -0,0 +1,89 @@ +import path from 'node:path' +import { Config, readConfigDirectory } from './config.js' +import { StructError } from 'superstruct' +import pino from 'pino' +import { startServer } from './server.js' +import { getPort } from './environment.js' +import { KubeConfig } from '@kubernetes/client-node' +import { loadKubeConfig } from './kube-config.js' + +const log = pino({ + level: 'info', + // do not log pid and hostname + base: undefined, + // use ISO strings for timestamps instead of milliseconds + timestamp: pino.stdTimeFunctions.isoTime, + // use string levels (e.g., "info") instead of level numbers (e.g., 30) + formatters: { + level: (label) => ({ level: label }), + log: (object) => { + if (object instanceof Error) { + return pino.stdSerializers.errWithCause(object) + } + return object + } + } +}) + +log.info('process_start') + +let config: Config +try { + config = await readConfigDirectory(path.join(process.cwd(), 'config')) +} catch (error) { + if (error instanceof StructError) { + // the value may contain secrets, so don't log it + error.value = undefined + error.branch = [] + } + log.fatal(error, 'config_error') + process.exit(1) +} + +let port: number +try { + port = getPort() +} catch (error) { + log.fatal(error, 'config_error') + process.exit(1) +} + +let kubeConfig: KubeConfig +try { + kubeConfig = loadKubeConfig(log, config) +} catch (error) { + log.fatal(error, 'config_error') + process.exit(1) +} + +const abortController = new AbortController() +for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.once(signal, () => { + log.info({ signal }, 'process_signal') + abortController.abort() + }) +} + +try { + const close = await startServer({ + log, + config, + port, + kubeConfig + }) + + if (abortController.signal.aborted) { + await close() + } else { + abortController.signal.addEventListener('abort', () => { + close().then(() => { + log.info('server_closed') + }).catch((error) => { + log.error(error, 'server_close_error') + }) + }) + } +} catch (error) { + log.fatal(error, 'uncaught_error') + process.exit(1) +} diff --git a/src/server.ts b/src/server.ts index c82f28b..a771366 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,49 +4,20 @@ import { backend, notFound } from 'backend' import { fastifyStatic } from '@fastify/static' import path from 'node:path' import { fastify } from 'fastify' -import { Config, readConfigDirectory } from './config.js' -import { getPort } from './environment.js' -import { loadKubeConfig } from './kube-config.js' -import { StructError } from 'superstruct' +import { Config } from './config.js' +import { KubeConfig } from '@kubernetes/client-node' import { handleError } from './handle-error.js' -import pino from 'pino' +import { BaseLogger } from 'pino' -const log = pino({ - level: 'info', - // do not log pid and hostname - base: undefined, - // use ISO strings for timestamps instead of milliseconds - timestamp: pino.stdTimeFunctions.isoTime, - // use string levels (e.g., "info") instead of level numbers (e.g., 30) - formatters: { - level: (label) => ({ level: label }), - log: (object) => { - if (object instanceof Error) { - return pino.stdSerializers.errWithCause(object) - } - return object - } - } -}) - -log.info('process_start') - -let config: Config -try { - config = await readConfigDirectory(path.join(process.cwd(), 'config')) -} catch (error) { - if (error instanceof StructError) { - // the value may contain secrets, so don't log it - error.value = undefined - error.branch = [] - } - log.fatal(error, 'config_error') - process.exit(1) -} +type CloseFunction = () => Promise -try { - const port = getPort() - const kubeConfig = loadKubeConfig(log, config) +export async function startServer (options: { + log: BaseLogger + config: Config + port: number + kubeConfig: KubeConfig +}): Promise { + const { log, config, port, kubeConfig } = options const app = fastify({ logger: log @@ -85,7 +56,8 @@ try { }) await app.listen({ port, host: '::' }) -} catch (error) { - log.fatal(error, 'uncaught_error') - process.exit(1) + + return async () => { + await app.close() + } }