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