From f0c52d8a8b82f9a543c6633306c4fb92a6fbb27d Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Thu, 28 Mar 2024 17:03:37 -0400 Subject: [PATCH] feat: make the config patchable --- app/root.tsx | 5 +- app/routes/_data.dns._index/rename.tsx | 44 ++++++++++----- app/routes/_data.dns._index/route.tsx | 11 +++- app/utils/config.ts | 75 ++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 01bf92c..5d4684d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -13,6 +13,7 @@ import clsx from 'clsx' import Toaster from '~/components/Toaster' import stylesheet from '~/tailwind.css?url' +import { getContext } from '~/utils/config' export const meta: MetaFunction = () => [ { title: 'Headplane' }, @@ -23,7 +24,9 @@ export const links: LinksFunction = () => [ { rel: 'stylesheet', href: stylesheet } ] -export function loader() { +export async function loader() { + await getContext() + if (!process.env.HEADSCALE_URL) { throw new Error('The HEADSCALE_URL environment variable is required') } diff --git a/app/routes/_data.dns._index/rename.tsx b/app/routes/_data.dns._index/rename.tsx index 2c91b7a..b72fa07 100644 --- a/app/routes/_data.dns._index/rename.tsx +++ b/app/routes/_data.dns._index/rename.tsx @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable unicorn/no-keyword-prefix */ import { Dialog } from '@headlessui/react' -import { Form } from '@remix-run/react' +import { useFetcher } from '@remix-run/react' import { useState } from 'react' type Properties = { @@ -8,6 +10,8 @@ type Properties = { export default function Modal({ name }: Properties) { const [isOpen, setIsOpen] = useState(false) + const [newName, setNewName] = useState(name) + const fetcher = useFetcher() return ( <> @@ -37,19 +41,31 @@ export default function Modal({ name }: Properties) { of unexpected behavior and may break existing devices in your tailnet. -
- - -
+ { + setNewName(event.target.value) + }} + /> + diff --git a/app/routes/_data.dns._index/route.tsx b/app/routes/_data.dns._index/route.tsx index 309dab6..e9e0dcc 100644 --- a/app/routes/_data.dns._index/route.tsx +++ b/app/routes/_data.dns._index/route.tsx @@ -1,9 +1,10 @@ import { Switch } from '@headlessui/react' -import { useLoaderData } from '@remix-run/react' +import { type ActionFunctionArgs } from '@remix-run/node' +import { json, useLoaderData } from '@remix-run/react' import clsx from 'clsx' import { useState } from 'react' -import { getConfig } from '~/utils/config' +import { getConfig, patchConfig } from '~/utils/config' import MagicModal from './magic' import RenameModal from './rename' @@ -25,6 +26,12 @@ export async function loader() { return dns } +export async function action({ request }: ActionFunctionArgs) { + const data = await request.json() as Record + await patchConfig(data) + return json({ success: true }) +} + export default function Page() { const data = useLoaderData() const [localOverride, setLocalOverride] = useState(data.overrideLocal) diff --git a/app/utils/config.ts b/app/utils/config.ts index af67bd4..b956b74 100644 --- a/app/utils/config.ts +++ b/app/utils/config.ts @@ -1,7 +1,7 @@ -import { readFile } from 'node:fs/promises' +import { access, constants, readFile, stat, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' -import { parse } from 'yaml' +import { type Document, parseDocument, visit } from 'yaml' type Duration = `${string}s` | `${string}h` | `${string}m` | `${string}d` | `${string}y` @@ -118,14 +118,79 @@ type Config = { randomize_client_port: boolean; } -let config: Config +let config: Document export async function getConfig() { if (!config) { const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml') const data = await readFile(path, 'utf8') - config = parse(data) as Config + config = parseDocument(data) } - return config + return config.toJSON() as Config } + +// This is so obscenely dangerous, please have a check around it +export async function patchConfig(partial: Record) { + for (const [key, value] of Object.entries(partial)) { + config.setIn(key.split('.'), value) + } + + const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml') + await writeFile(path, config.toString(), 'utf8') +} + +type Context = { + isDocker: boolean; + hasDockerSock: boolean; + hasConfigWrite: boolean; +} + +export let context: Context + +export async function getContext() { + if (!context) { + context = { + isDocker: await checkDocker(), + hasDockerSock: await checkSock(), + hasConfigWrite: await checkConfigWrite() + } + } + + return context +} + +async function checkConfigWrite() { + try { + await getConfig() + return true + } catch (error) { + console.error('Failed to read config file', error) + } + + return false +} + +async function checkSock() { + try { + await access('/var/run/docker.sock', constants.R_OK) + return true + } catch {} + + return false +} + +async function checkDocker() { + try { + await stat('/.dockerenv') + return true + } catch {} + + try { + const data = await readFile('/proc/self/cgroup', 'utf8') + return data.includes('docker') + } catch {} + + return false +} +