mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: support 0.26+ routes only
This commit is contained in:
@@ -46,16 +46,16 @@ export default function Routes({ node, isOpen, setIsOpen }: RoutesProps) {
|
||||
</TableList.Item>
|
||||
) : undefined}
|
||||
{subnets.map((route) => (
|
||||
<TableList.Item key={route.id}>
|
||||
<p>{route.prefix}</p>
|
||||
<TableList.Item key={route}>
|
||||
<p>{route}</p>
|
||||
<Switch
|
||||
defaultSelected={route.enabled}
|
||||
defaultSelected={node.approvedRoutes.includes(route)}
|
||||
label="Enabled"
|
||||
onChange={(checked) => {
|
||||
const form = new FormData();
|
||||
form.set('action_id', 'update_routes');
|
||||
form.set('node_id', node.id);
|
||||
form.set('routes', [route.id].join(','));
|
||||
form.set('routes', [route].join(','));
|
||||
|
||||
form.set('enabled', String(checked));
|
||||
fetcher.submit(form, {
|
||||
@@ -95,7 +95,7 @@ export default function Routes({ node, isOpen, setIsOpen }: RoutesProps) {
|
||||
form.set(
|
||||
'routes',
|
||||
node.customRouting.exitRoutes
|
||||
.map((route) => route.id)
|
||||
.map((route) => route)
|
||||
.join(','),
|
||||
);
|
||||
|
||||
|
||||
@@ -188,6 +188,12 @@ async function updateRoutes(
|
||||
nodeId: string,
|
||||
context: LoadContext,
|
||||
) {
|
||||
const { node } = await context.client.get<{ node: Machine }>(
|
||||
`v1/node/${nodeId}`,
|
||||
apiKey,
|
||||
);
|
||||
|
||||
const newApproved = node.approvedRoutes;
|
||||
const routes = formData.get('routes')?.toString();
|
||||
if (!routes) {
|
||||
throw data('Missing `routes` in the form data.', {
|
||||
@@ -209,12 +215,32 @@ async function updateRoutes(
|
||||
});
|
||||
}
|
||||
|
||||
const postfix = enabled === 'true' ? 'enable' : 'disable';
|
||||
await Promise.all(
|
||||
allRoutes.map(async (route) => {
|
||||
await context.client.post(`v1/routes/${route}/${postfix}`, apiKey);
|
||||
}),
|
||||
);
|
||||
if (enabled === 'true') {
|
||||
for (const route of allRoutes) {
|
||||
// If already approved, skip, otherwise add to approved
|
||||
if (newApproved.includes(route)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newApproved.push(route);
|
||||
}
|
||||
} else {
|
||||
for (const route of allRoutes) {
|
||||
// If not approved, skip, otherwise remove from approved
|
||||
if (!newApproved.includes(route)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = newApproved.indexOf(route);
|
||||
if (index > -1) {
|
||||
newApproved.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await context.client.post(`v1/node/${nodeId}/approve_routes`, apiKey, {
|
||||
routes: newApproved,
|
||||
});
|
||||
|
||||
return { message: 'Routes updated' };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Link from '~/components/Link';
|
||||
import StatusCircle from '~/components/StatusCircle';
|
||||
import Tooltip from '~/components/Tooltip';
|
||||
import type { LoadContext } from '~/server';
|
||||
import type { Machine, Route, User } from '~/types';
|
||||
import type { Machine, User } from '~/types';
|
||||
import cn from '~/utils/cn';
|
||||
import { getOSInfo, getTSVersion } from '~/utils/host-info';
|
||||
import { mapNodes } from '~/utils/node-info';
|
||||
@@ -37,20 +37,16 @@ export async function loader({
|
||||
}
|
||||
}
|
||||
|
||||
const [machine, { routes }, { users }] = await Promise.all([
|
||||
const [machine, { users }] = await Promise.all([
|
||||
context.client.get<{ node: Machine }>(
|
||||
`v1/node/${params.id}`,
|
||||
session.get('api_key')!,
|
||||
),
|
||||
context.client.get<{ routes: Route[] }>(
|
||||
'v1/routes',
|
||||
session.get('api_key')!,
|
||||
),
|
||||
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
|
||||
]);
|
||||
|
||||
const [node] = mapNodes([machine.node], routes);
|
||||
const lookup = await context.agents?.lookup([node.nodeKey]);
|
||||
const lookup = await context.agents?.lookup([machine.node.nodeKey]);
|
||||
const [node] = mapNodes([machine.node], lookup);
|
||||
|
||||
return {
|
||||
node,
|
||||
@@ -160,7 +156,7 @@ export default function Page() {
|
||||
) : (
|
||||
<ul className="leading-normal">
|
||||
{node.customRouting.subnetApprovedRoutes.map((route) => (
|
||||
<li key={route.id}>{route.prefix}</li>
|
||||
<li key={route}>{route}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@@ -192,7 +188,7 @@ export default function Page() {
|
||||
) : (
|
||||
<ul className="leading-normal">
|
||||
{node.customRouting.subnetWaitingRoutes.map((route) => (
|
||||
<li key={route.id}>{route.prefix}</li>
|
||||
<li key={route}>{route}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { InfoIcon } from '@primer/octicons-react';
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
||||
import { useLoaderData } from 'react-router';
|
||||
import Code from '~/components/Code';
|
||||
import { ErrorPopup } from '~/components/Error';
|
||||
import Link from '~/components/Link';
|
||||
import Tooltip from '~/components/Tooltip';
|
||||
import type { LoadContext } from '~/server';
|
||||
import { Capabilities } from '~/server/web/roles';
|
||||
import type { Machine, Route, User } from '~/types';
|
||||
import type { Machine, User } from '~/types';
|
||||
import cn from '~/utils/cn';
|
||||
import { mapNodes } from '~/utils/node-info';
|
||||
import MachineRow from './components/machine-row';
|
||||
@@ -41,15 +40,11 @@ export async function loader({
|
||||
Capabilities.write_machines,
|
||||
);
|
||||
|
||||
const [{ nodes }, { routes }, { users }] = await Promise.all([
|
||||
const [{ nodes }, { users }] = await Promise.all([
|
||||
context.client.get<{ nodes: Machine[] }>(
|
||||
'v1/node',
|
||||
session.get('api_key')!,
|
||||
),
|
||||
context.client.get<{ routes: Route[] }>(
|
||||
'v1/routes',
|
||||
session.get('api_key')!,
|
||||
),
|
||||
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
|
||||
]);
|
||||
|
||||
@@ -61,12 +56,11 @@ export async function loader({
|
||||
}
|
||||
|
||||
const stats = await context.agents?.lookup(nodes.map((node) => node.nodeKey));
|
||||
const populatedNodes = mapNodes(nodes, routes, stats);
|
||||
const populatedNodes = mapNodes(nodes, stats);
|
||||
|
||||
return {
|
||||
populatedNodes,
|
||||
nodes,
|
||||
routes,
|
||||
users,
|
||||
magic,
|
||||
server: context.config.headscale.url,
|
||||
|
||||
@@ -26,4 +26,9 @@ export interface Machine {
|
||||
validTags: string[];
|
||||
givenName: string;
|
||||
online: boolean;
|
||||
|
||||
// Added in Headscale 0.26+
|
||||
approvedRoutes: string[];
|
||||
availableRoutes: string[];
|
||||
subnetRoutes: string[];
|
||||
}
|
||||
|
||||
+25
-31
@@ -1,52 +1,46 @@
|
||||
import { HostInfo, Machine, Route } from '~/types';
|
||||
|
||||
export interface PopulatedNode extends Machine {
|
||||
routes: Route[];
|
||||
routes: string[];
|
||||
hostInfo?: HostInfo;
|
||||
expired: boolean;
|
||||
customRouting: {
|
||||
exitRoutes: Route[];
|
||||
exitRoutes: string[];
|
||||
exitApproved: boolean;
|
||||
subnetApprovedRoutes: Route[];
|
||||
subnetWaitingRoutes: Route[];
|
||||
subnetApprovedRoutes: string[];
|
||||
subnetWaitingRoutes: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function mapNodes(
|
||||
nodes: Machine[],
|
||||
routes: Route[],
|
||||
stats?: Record<string, HostInfo> | undefined,
|
||||
): PopulatedNode[] {
|
||||
return nodes.map((node) => {
|
||||
const nodeRoutes = routes.filter((route) => route.node.id === node.id);
|
||||
const customRouting = nodeRoutes.reduce<PopulatedNode['customRouting']>(
|
||||
(acc, route) => {
|
||||
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') {
|
||||
acc.exitRoutes.push(route);
|
||||
if (route.enabled) {
|
||||
acc.exitApproved = true;
|
||||
}
|
||||
} else {
|
||||
if (route.enabled) {
|
||||
acc.subnetApprovedRoutes.push(route);
|
||||
} else {
|
||||
acc.subnetWaitingRoutes.push(route);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
exitRoutes: [],
|
||||
exitApproved: false,
|
||||
subnetApprovedRoutes: [],
|
||||
subnetWaitingRoutes: [],
|
||||
},
|
||||
);
|
||||
const customRouting = {
|
||||
exitRoutes: node.availableRoutes.filter(
|
||||
(route) => route === '::/0' || route === '0.0.0.0/0',
|
||||
),
|
||||
exitApproved: node.approvedRoutes.some(
|
||||
(route) => route === '::/0' || route === '0.0.0.0/0',
|
||||
),
|
||||
subnetApprovedRoutes: node.approvedRoutes.filter(
|
||||
(route) =>
|
||||
route !== '::/0' &&
|
||||
route !== '0.0.0.0/0' &&
|
||||
node.availableRoutes.includes(route),
|
||||
),
|
||||
subnetWaitingRoutes: node.availableRoutes.filter(
|
||||
(route) =>
|
||||
route !== '::/0' &&
|
||||
route !== '0.0.0.0/0' &&
|
||||
!node.approvedRoutes.includes(route),
|
||||
),
|
||||
} satisfies PopulatedNode['customRouting'];
|
||||
|
||||
return {
|
||||
...node,
|
||||
routes: nodeRoutes,
|
||||
routes: Array.from(new Set(node.availableRoutes)),
|
||||
hostInfo: stats?.[node.nodeKey],
|
||||
customRouting,
|
||||
expired:
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ networks:
|
||||
driver: "bridge"
|
||||
services:
|
||||
headscale:
|
||||
image: "headscale/headscale:0.25.1"
|
||||
image: "headscale/headscale:0.26.0-debug"
|
||||
container_name: "headscale"
|
||||
labels:
|
||||
me.tale.headplane.target: headscale
|
||||
@@ -21,5 +21,6 @@ services:
|
||||
- "./test:/etc/headscale"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "9090:9090"
|
||||
environment:
|
||||
TZ: "America/New_York"
|
||||
|
||||
Reference in New Issue
Block a user