From e216d4171fd5303ea01f3d8a55d0d2ddc7f5da46 Mon Sep 17 00:00:00 2001
From: Aarnav Tale
Date: Fri, 29 Mar 2024 21:46:21 -0400
Subject: [PATCH] feat: support readonly config and no docker sock
---
app/components/Notice.tsx | 16 +++++
app/routes/_data.dns._index/domains.tsx | 90 +++++++++++++++----------
app/routes/_data.dns._index/magic.tsx | 5 +-
app/routes/_data.dns._index/rename.tsx | 7 +-
app/routes/_data.dns._index/route.tsx | 83 ++++++++++++++---------
app/utils/config.ts | 19 +-----
app/utils/docker.ts | 6 ++
7 files changed, 139 insertions(+), 87 deletions(-)
create mode 100644 app/components/Notice.tsx
diff --git a/app/components/Notice.tsx b/app/components/Notice.tsx
new file mode 100644
index 0000000..6c593c0
--- /dev/null
+++ b/app/components/Notice.tsx
@@ -0,0 +1,16 @@
+import { InformationCircleIcon } from '@heroicons/react/24/outline'
+import clsx from 'clsx'
+import { type ReactNode } from 'react'
+
+export default function Notice({ children }: { readonly children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
diff --git a/app/routes/_data.dns._index/domains.tsx b/app/routes/_data.dns._index/domains.tsx
index 1538fcf..f13d3f4 100644
--- a/app/routes/_data.dns._index/domains.tsx
+++ b/app/routes/_data.dns._index/domains.tsx
@@ -27,9 +27,11 @@ import TableList from '~/components/TableList'
type Properties = {
readonly baseDomain?: string;
readonly searchDomains: string[];
+ // eslint-disable-next-line react/boolean-prop-naming
+ readonly disabled?: boolean;
}
-export default function Domains({ baseDomain, searchDomains }: Properties) {
+export default function Domains({ baseDomain, searchDomains, disabled }: Properties) {
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/ban-types
const [activeId, setActiveId] = useState(null)
const [localDomains, setLocalDomains] = useState(searchDomains)
@@ -88,8 +90,14 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
strategy={verticalListSortingStrategy}
>
{localDomains.map((sd, index) => (
- // eslint-disable-next-line react/no-array-index-key
-
+
))}
{activeId ? : undefined}
-
- {
- setNewDomain(event.target.value)
- }}
- />
-
+
+ )}
@@ -140,9 +151,11 @@ type DomainProperties = {
readonly id: number;
readonly isDrag?: boolean;
readonly localDomains: string[];
+ // eslint-disable-next-line react/boolean-prop-naming
+ readonly disabled?: boolean;
}
-function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
+function Domain({ domain, id, localDomains, isDrag, disabled }: DomainProperties) {
const fetcher = useFetcher()
const {
@@ -169,17 +182,20 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
}}
>
-
+ {disabled ? undefined : (
+
+ )}
{domain}
{isDrag ? undefined : (
{
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/app/routes/_data.dns._index/magic.tsx b/app/routes/_data.dns._index/magic.tsx
index d980e1f..6d55b21 100644
--- a/app/routes/_data.dns._index/magic.tsx
+++ b/app/routes/_data.dns._index/magic.tsx
@@ -7,9 +7,11 @@ import Button from '~/components/Button'
type Properties = {
readonly isEnabled: boolean;
+ // eslint-disable-next-line react/boolean-prop-naming
+ readonly disabled?: boolean;
}
-export default function Modal({ isEnabled }: Properties) {
+export default function Modal({ isEnabled, disabled }: Properties) {
const [isOpen, setIsOpen] = useState(false)
const fetcher = useFetcher()
@@ -18,6 +20,7 @@ export default function Modal({ isEnabled }: Properties) {
{
setIsOpen(true)
}}
diff --git a/app/routes/_data.dns._index/rename.tsx b/app/routes/_data.dns._index/rename.tsx
index 9884fb0..8ad5c63 100644
--- a/app/routes/_data.dns._index/rename.tsx
+++ b/app/routes/_data.dns._index/rename.tsx
@@ -10,9 +10,11 @@ import Input from '~/components/Input'
type Properties = {
readonly name: string;
+ // eslint-disable-next-line react/boolean-prop-naming
+ readonly disabled?: boolean;
}
-export default function Modal({ name }: Properties) {
+export default function Modal({ name, disabled }: Properties) {
const [isOpen, setIsOpen] = useState(false)
const [newName, setNewName] = useState(name)
const fetcher = useFetcher()
@@ -32,7 +34,7 @@ export default function Modal({ name }: Properties) {
{
@@ -42,6 +44,7 @@ export default function Modal({ name }: Properties) {
{
setIsOpen(true)
}}
diff --git a/app/routes/_data.dns._index/route.tsx b/app/routes/_data.dns._index/route.tsx
index 4652031..21b1abe 100644
--- a/app/routes/_data.dns._index/route.tsx
+++ b/app/routes/_data.dns._index/route.tsx
@@ -7,8 +7,9 @@ import { useState } from 'react'
import Button from '~/components/Button'
import Code from '~/components/Code'
import Input from '~/components/Input'
+import Notice from '~/components/Notice'
import TableList from '~/components/TableList'
-import { getConfig, patchConfig } from '~/utils/config'
+import { getConfig, getContext, patchConfig } from '~/utils/config'
import { restartHeadscale } from '~/utils/docker'
import Domains from './domains'
@@ -18,6 +19,8 @@ import RenameModal from './rename'
// We do not want to expose every config value
export async function loader() {
const config = await getConfig()
+ const context = await getContext()
+
const dns = {
prefixes: config.prefixes,
magicDns: config.dns_config.magic_dns,
@@ -29,10 +32,18 @@ export async function loader() {
extraRecords: config.dns_config.extra_records
}
- return dns
+ return {
+ ...dns,
+ ...context
+ }
}
export async function action({ request }: ActionFunctionArgs) {
+ const context = await getContext()
+ if (!context.hasConfigWrite) {
+ return json({ success: false })
+ }
+
const data = await request.json() as Record
await patchConfig(data)
await restartHeadscale()
@@ -47,7 +58,12 @@ export default function Page() {
return (
-
+ {data.hasConfigWrite ? undefined : (
+
+ The Headscale configuration is read-only. You cannot make changes to the configuration
+
+ )}
+
Nameservers
@@ -63,6 +79,7 @@ export default function Page() {
Override local DNS
{
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -111,35 +129,37 @@ export default function Page() {
))}
-
- {
- setNs(event.target.value)
- }}
- />
- {
- fetcher.submit({
+ {data.hasConfigWrite ? (
+
+ {
+ setNs(event.target.value)
+ }}
+ />
+ {
+ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
- 'dns_config.nameservers': [...data.nameservers, ns]
- }, {
- method: 'PATCH',
- encType: 'application/json'
- })
+ 'dns_config.nameservers': [...data.nameservers, ns]
+ }, {
+ method: 'PATCH',
+ encType: 'application/json'
+ })
- setNs('')
- }}
- >
- Add
-
-
+ setNs('')
+ }}
+ >
+ Add
+
+
+ ) : undefined}
{/* TODO: Split DNS and Custom A Records */}
@@ -148,6 +168,7 @@ export default function Page() {
@@ -162,7 +183,7 @@ export default function Page() {
{' '}
when Magic DNS is enabled.
-
+
)
diff --git a/app/utils/config.ts b/app/utils/config.ts
index 1da31e3..f6ac9cb 100644
--- a/app/utils/config.ts
+++ b/app/utils/config.ts
@@ -141,7 +141,6 @@ export async function patchConfig(partial: Record) {
}
type Context = {
- isDocker: boolean;
hasDockerSock: boolean;
hasConfigWrite: boolean;
}
@@ -151,7 +150,6 @@ export let context: Context
export async function getContext() {
if (!context) {
context = {
- isDocker: await checkDocker(),
hasDockerSock: await checkSock(),
hasConfigWrite: await checkConfigWrite()
}
@@ -177,20 +175,9 @@ async function checkSock() {
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 {}
+ if (!process.env.HEADSCALE_CONTAINER) {
+ return false
+ }
return false
}
-
diff --git a/app/utils/docker.ts b/app/utils/docker.ts
index 6861685..422238f 100644
--- a/app/utils/docker.ts
+++ b/app/utils/docker.ts
@@ -5,9 +5,15 @@ import { setTimeout } from 'node:timers/promises'
import { Client } from 'undici'
+import { getContext } from './config'
import { pull } from './headscale'
export async function restartHeadscale() {
+ const context = await getContext()
+ if (!context.hasDockerSock) {
+ return
+ }
+
if (!process.env.HEADSCALE_CONTAINER) {
throw new Error('HEADSCALE_CONTAINER is not set')
}