From ed50c48965d46ba1a236704531bbcecdf77505bf Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Thu, 30 May 2024 10:15:02 -0400 Subject: [PATCH] fix: handle configs better and propagate errors --- app/entry.server.tsx | 62 +++++++++++++++++++++++++++++++++++ app/utils/config/headplane.ts | 30 ++++++++++++----- app/utils/config/headscale.ts | 32 +++++++++++++++--- 3 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 app/entry.server.tsx diff --git a/app/entry.server.tsx b/app/entry.server.tsx new file mode 100644 index 0000000..f6c9843 --- /dev/null +++ b/app/entry.server.tsx @@ -0,0 +1,62 @@ +import { PassThrough } from 'node:stream' + +import type { AppLoadContext, EntryContext } from '@remix-run/node' +import { createReadableStreamFromReadable } from '@remix-run/node' +import { RemixServer } from '@remix-run/react' +import { isbot } from 'isbot' +import { renderToPipeableStream } from 'react-dom/server' + +import { loadContext } from './utils/config/headplane' + +await loadContext() + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _loadContext: AppLoadContext, +) { + const ua = request.headers.get('user-agent') + const isBot = ua ? isbot(ua) : false + + return new Promise((resolve, reject) => { + let shellRendered = false + const { pipe, abort } = renderToPipeableStream( + , + { + [isBot ? 'onAllReady' : 'onShellReady']() { + shellRendered = true + const body = new PassThrough() + const stream = createReadableStreamFromReadable(body) + responseHeaders.set('Content-Type', 'text/html') + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError(error: unknown) { + reject(error as Error) + }, + onError(error: unknown) { + responseStatusCode = 500 + if (shellRendered) { + console.error(error) + } + }, + }, + ) + + setTimeout(abort, 5000) + }) +} diff --git a/app/utils/config/headplane.ts b/app/utils/config/headplane.ts index 83646fe..de31b46 100644 --- a/app/utils/config/headplane.ts +++ b/app/utils/config/headplane.ts @@ -43,12 +43,8 @@ export async function loadContext(): Promise { return context } - let config: HeadscaleConfig | undefined - try { - config = await loadConfig() - } catch {} - const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml') + const { config, contextData } = await checkConfig(path) let headscaleUrl = process.env.HEADSCALE_URL if (!headscaleUrl && !config) { @@ -72,7 +68,7 @@ export async function loadContext(): Promise { headscaleUrl, cookieSecret, integration: await checkIntegration(), - config: await checkConfig(path, config), + config: contextData, acl: await checkAcl(config), oidc: await checkOidc(config), } @@ -121,7 +117,20 @@ export async function patchAcl(data: string) { await writeFile(path, data, 'utf8') } -async function checkConfig(path: string, config?: HeadscaleConfig) { +async function checkConfig(path: string) { + let config: HeadscaleConfig | undefined + try { + config = await loadConfig(path) + } catch { + return { + config: undefined, + contextData: { + read: false, + write: false, + }, + } + } + let write = false try { await access(path, constants.W_OK) @@ -129,8 +138,11 @@ async function checkConfig(path: string, config?: HeadscaleConfig) { } catch {} return { - read: config ? true : false, - write, + config, + contextData: { + read: true, + write, + }, } } diff --git a/app/utils/config/headscale.ts b/app/utils/config/headscale.ts index fd6f61f..4bd3e81 100644 --- a/app/utils/config/headscale.ts +++ b/app/utils/config/headscale.ts @@ -158,16 +158,40 @@ export type HeadscaleConfig = z.infer export let configYaml: Document | undefined export let config: HeadscaleConfig | undefined -export async function loadConfig() { +export async function loadConfig(path?: string) { if (config) { return config } - const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml') - const data = await readFile(path, 'utf8') + if (!path) { + throw new Error('Path is required to lazy load config') + } + const data = await readFile(path, 'utf8') configYaml = parseDocument(data) - config = await HeadscaleConfig.parseAsync(configYaml.toJSON()) + + try { + config = await HeadscaleConfig.parseAsync(configYaml.toJSON()) + } catch (error) { + if (error instanceof z.ZodError) { + console.log('Failed to parse the Headscale configuration file!') + console.log('The following schema issues were found:') + for (const issue of error.issues) { + const path = issue.path.map(String).join('.') + const message = issue.message + + console.log(`- '${path}': ${message}`) + } + + console.log('') + console.log('Please fix the configuration file and try again.') + console.log('Headplane will operate as if no config is present.') + console.log('') + } + + throw error + } + return config }