feat: make the config patchable
This commit is contained in:
parent
c6435ae2b4
commit
f0c52d8a8b
@ -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')
|
||||
}
|
||||
|
||||
@ -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.
|
||||
</Dialog.Description>
|
||||
<Form method='PATCH'>
|
||||
<input
|
||||
type='text'
|
||||
className='border rounded-lg p-2 w-full mt-4'
|
||||
placeholder={name}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='rounded-lg py-2 bg-gray-800 text-white w-full mt-2'
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
</Form>
|
||||
<input
|
||||
type='text'
|
||||
className='border rounded-lg p-2 w-full mt-4'
|
||||
value={newName}
|
||||
onChange={event => {
|
||||
setNewName(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type='submit'
|
||||
className='rounded-lg py-2 bg-gray-800 text-white w-full mt-2'
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
'dns_config.base_domain': newName
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
|
||||
setIsOpen(false)
|
||||
setNewName(name)
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@ -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<string, unknown>
|
||||
await patchConfig(data)
|
||||
return json({ success: true })
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const data = useLoaderData<typeof loader>()
|
||||
const [localOverride, setLocalOverride] = useState(data.overrideLocal)
|
||||
|
||||
@ -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<string, unknown>) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user