Compare commits

..

8 Commits

Author SHA1 Message Date
Aarnav Tale 6c83e96341 chore: v0.5.2 2025-02-28 13:07:14 -05:00
Aarnav Tale 03e6a26b3a fix: don't reload the prod bundle on refresh 2025-02-28 13:06:38 -05:00
Aarnav Tale 5b716ab5ce chore: update docs to reflect 0.5.1 2025-02-28 12:22:20 -05:00
Aarnav Tale a7e4f3e4d2 chore: v0.5.1 2025-02-28 12:21:00 -05:00
Aarnav Tale 40ddb69bc9 fix: resolve latest tags being set 2025-02-28 12:19:14 -05:00
Aarnav Tale 6aa6e77611 chore: fix globalThis typings 2025-02-28 12:17:58 -05:00
Aarnav Tale c5c2f8a93b fix: use context for k8s config 2025-02-28 11:29:31 -05:00
Aarnav Tale acd042b4de fix: define globals correctly 2025-02-28 11:29:11 -05:00
9 changed files with 75 additions and 21 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=false
- name: Build and push Docker image
uses: docker/build-push-action@v5
+8
View File
@@ -1,3 +1,11 @@
### 0.5.2 (February 28, 2025)
- Hotfixed an issue where the server bundle got reloaded on each request
### 0.5.1 (February 28, 2025)
- Fixed an issue that caused the entire server to crash on start
- Fixed the published semver tags from Docker
- Fixed the Kubernetes integration not reading the config
### 0.5 (February 27, 2025)
- Completely redesigned the UI from the ground up for accessibility and performance.
- Switched to a config-file setup (this introduces breaking changes, see [config.example.yaml](/config.example.yaml) for the new format).
+2 -2
View File
@@ -58,10 +58,10 @@ export default class KubernetesIntegration extends Integration<T> {
);
// Some very ugly nesting but it's necessary
if (process.env.HEADSCALE_INTEGRATION_UNSTRICT === 'true') {
if (this.context.validate_manifest === false) {
log.warn('INTG', 'Skipping strict Pod status check');
} else {
const pod = process.env.POD_NAME;
const pod = this.context.pod_name;
if (!pod) {
log.error('INTG', 'Missing POD_NAME variable');
return false;
+35
View File
@@ -0,0 +1,35 @@
# Headplane Agent
The Headplane agent is a lightweight service that runs alongside the Headscale server.
It's used to interface with devices on your network locally, unlocking the following:
- **Node Information/Status**: View Tailscale versions, OS versions, and connection details.
- **SSH via Web (Soon)**: Connect to devices via SSH directly from the Headplane UI.
It's built on top of [tsnet](https://tailscale.com/kb/1244/tsnet), the official
set of libraries published by Tailscale for creating local services that can
join the tailnet.
While it isn't required to run the agent, it's highly recommended to get the
closest experience to the SaaS version of Tailscale. This is paired with the
integrations provided by Headplane to manage DNS and Headscale settings.
### Installation
The agent can either be ran as a standalone binary or as a Docker container.
Agent binaries are available on the [releases](https://github.com/tale/headplane/releases) page.
The Docker image is available through the `ghcr.io/tale/headplane-agent` tag.
The agent requires the following environment variables to be set:
- **`HP_AGENT_HOSTNAME`**: A hostname you want to use for the agent.
- **`HP_AGENT_TS_SERVER`**: The URL to your Headscale instance.
- **`HP_AGENT_TS_AUTHKEY`**: An authorization key to authenticate with Headscale (see below).
- **`HP_AGENT_HP_SERVER`**: The URL to your Headplane instance.
- **`HP_AGENT_HP_AUTHKEY`**: The generated auth key to authenticate with Headplane.
If you already have Headplane setup, you can generate all of these values within
the Headplane UI. Navigate to the `Settings` page and click `Agent` to get started.
HP_AGENT_HOSTNAME=headplane-agent
HP_AGENT_TS_SERVER=http://localhost:8080
#HP_AGENT_AUTH_KEY=3e0cd749021e5984267cde4b0a5a2ac32c1859e56f7911aa
HP_AGENT_TS_AUTHKEY=a4dab065c735cb4eae4f12804cf7e111206f9c7c9247c629
HP_AGENT_HP_SERVER=http://localhost:3000/admin
+2 -2
View File
@@ -34,7 +34,7 @@ Here is what a sample Docker Compose deployment would look like:
services:
headplane:
# I recommend you pin the version to a specific release
image: ghcr.io/tale/headplane:0.5.0
image: ghcr.io/tale/headplane:0.5.1
container_name: headplane
restart: unless-stopped
ports:
@@ -148,7 +148,7 @@ spec:
serviceAccountName: default
containers:
- name: headplane
image: ghcr.io/tale/headplane:0.5.0
image: ghcr.io/tale/headplane:0.5.1
env:
# Set these if the pod name for Headscale is not static
# We will use the downward API to get the pod name instead
+1 -1
View File
@@ -19,7 +19,7 @@ Here is what a sample Docker Compose deployment would look like:
services:
headplane:
# I recommend you pin the version to a specific release
image: ghcr.io/tale/headplane:0.5.0
image: ghcr.io/tale/headplane:0.5.1
container_name: headplane
restart: unless-stopped
ports:
+6 -9
View File
@@ -1,4 +1,5 @@
import { constants, access, readFile } from 'node:fs/promises';
import { env } from 'node:process';
import { type } from 'arktype';
import dotenv from 'dotenv';
import { parseDocument } from 'yaml';
@@ -7,7 +8,7 @@ import log, { hpServer_loadLogger } from '~server/utils/log';
import mutex from '~server/utils/mutex';
import { HeadplaneConfig, coalesceConfig, validateConfig } from './parser';
declare global {
declare namespace globalThis {
let __cookie_context: {
cookie_secret: string;
cookie_secure: boolean;
@@ -63,9 +64,9 @@ export async function hp_loadConfig() {
let path = HEADPLANE_DEFAULT_CONFIG_PATH;
const envs = rootEnvs({
HEADPLANE_DEBUG_LOG: process.env.HEADPLANE_DEBUG_LOG,
HEADPLANE_CONFIG_PATH: process.env.HEADPLANE_CONFIG_PATH,
HEADPLANE_LOAD_ENV_OVERRIDES: process.env.HEADPLANE_LOAD_ENV_OVERRIDES,
HEADPLANE_DEBUG_LOG: env.HEADPLANE_DEBUG_LOG,
HEADPLANE_CONFIG_PATH: env.HEADPLANE_CONFIG_PATH,
HEADPLANE_LOAD_ENV_OVERRIDES: env.HEADPLANE_LOAD_ENV_OVERRIDES,
});
if (envs instanceof type.errors) {
@@ -79,7 +80,6 @@ export async function hp_loadConfig() {
// Load our debug based logger before ANYTHING
hpServer_loadLogger(envs.HEADPLANE_DEBUG_LOG);
if (envs.HEADPLANE_CONFIG_PATH) {
path = envs.HEADPLANE_CONFIG_PATH;
}
@@ -117,20 +117,17 @@ export async function hp_loadConfig() {
testOidc(config.oidc);
}
// @ts-expect-error: If we remove globalThis we get a runtime error
globalThis.__cookie_context = {
cookie_secret: config.server.cookie_secret,
cookie_secure: config.server.cookie_secure,
};
// @ts-expect-error: If we remove globalThis we get a runtime error
globalThis.__hs_context = {
url: config.headscale.url,
config_path: config.headscale.config_path,
config_strict: config.headscale.config_strict,
};
// @ts-expect-error: If we remove globalThis we get a runtime error
globalThis.__integration_context = config.integration;
runtimeConfig = config;
@@ -190,7 +187,7 @@ function coalesceEnv(config: HeadplaneConfig) {
const rootKeys: string[] = rootEnvs.props.map((prop) => prop.key);
// Typescript is still insanely stupid at nullish filtering
const vars = Object.entries(process.env).filter(([key, value]) => {
const vars = Object.entries(env).filter(([key, value]) => {
if (!value) {
return false;
}
+1
View File
@@ -37,6 +37,7 @@ const dockerConfig = type({
const kubernetesConfig = type({
enabled: stringToBool,
pod_name: 'string',
validate_manifest: stringToBool,
});
+19 -6
View File
@@ -6,29 +6,42 @@ import {
writeReadableStreamToWritable,
} from '@react-router/node';
import mime from 'mime/lite';
import type { RequestHandler } from 'react-router';
import type { ViteDevServer } from 'vite';
import appContext from '~server/context/app';
import { loadDevtools, stacksafeTry } from '~server/dev/hot-server';
import prodBuild from '~server/prod-handler';
import { hp_loadConfig } from './context/loader';
declare global {
// Prefix is a build-time constant
const __hp_prefix: string;
}
const devtools = import.meta.env.DEV ? await loadDevtools() : undefined;
// biome-ignore lint/suspicious/noExplicitAny: Who cares man
let devtools: { server: ViteDevServer; handler: any } | undefined = undefined;
let prodHandler: RequestHandler | undefined = undefined;
let loaded = false;
const prodHandler = import.meta.env.PROD ? await prodBuild() : undefined;
const buildPath = process.env.BUILD_PATH ?? './build';
const baseDir = resolve(join(buildPath, 'client'));
async function loadGlobals() {
await hp_loadConfig();
devtools = import.meta.env.DEV ? await loadDevtools() : undefined;
prodHandler = import.meta.env.PROD ? await prodBuild() : undefined;
loaded = true;
}
const baseDir = resolve(join('./build', 'client'));
export const listener: RequestListener = async (req, res) => {
if (!loaded) {
await loadGlobals();
}
const url = new URL(`http://${req.headers.host}${req.url}`);
// build:strip
if (devtools) {
await new Promise((resolve) => {
devtools.server.middlewares(req, res, resolve);
devtools?.server.middlewares(req, res, resolve);
});
}