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
+}
+