feat: bump headscale minimum to 0.27

This commit is contained in:
Aarnav Tale
2026-05-30 17:32:00 -04:00
parent 7901f37002
commit d7f1d665a4
11 changed files with 101 additions and 112 deletions
-8
View File
@@ -36,13 +36,6 @@ export interface Capabilities {
* 0.28.0+.
*/
readonly nodeOwnerIsImmutable: boolean;
/**
* Policy parse errors use the modern `parsing HuJSON:` /
* `parsing policy from bytes:` prefixes. Pre-0.27 emitted
* `parsing hujson` / `unmarshalling policy`. Introduced in 0.27.0.
*/
readonly policyErrorsUseModernFormat: boolean;
}
export function capabilitiesFor(version: ServerVersion): Capabilities {
@@ -50,6 +43,5 @@ export function capabilitiesFor(version: ServerVersion): Capabilities {
preAuthKeysHaveStableIds: gte(version, "0.28.0"),
nodeTagsAreFlat: gte(version, "0.28.0"),
nodeOwnerIsImmutable: gte(version, "0.28.0"),
policyErrorsUseModernFormat: gte(version, "0.27.0"),
};
}
+47 -18
View File
@@ -1,14 +1,26 @@
// MARK: Headscale API
//
// The public entry point for talking to a Headscale server. Boot
// detects the server version via `GET /version` (unauthenticated,
// available since Headscale 0.26.0), derives a typed `Capabilities`
// object, and returns a `Headscale` value that constructs
// authenticated `HeadscaleClient`s on demand.
// The public entry point for talking to a Headscale server. At boot
// we try `GET /version` (unauthenticated, present since Headscale
// 0.27.0 — the minimum version Headplane supports) to derive a
// typed `Capabilities` object. Boot outcomes:
//
// - success: parse the response, derive capabilities, done.
// - 404: Headscale is reachable but predates 0.27.0 and is no
// longer supported. Log an error and keep retrying so an
// upgrade is picked up without a Headplane restart.
// - any other failure (network, 5xx, parse): Headplane still
// boots with `version = unknown` (capabilities-permissive) and
// a background retry. This handles docker-compose start-order
// races without making the whole process unhappy.
//
// Capabilities are always derived from `version`; once detection
// finishes there's no further state to track.
import log from "~/utils/log";
import { type Capabilities, capabilitiesFor } from "./capabilities";
import { isDataWithApiError } from "./error-client";
import { type ApiKeyApi, makeApiKeyApi } from "./resources/api-keys";
import { makeNodeApi, type NodeApi } from "./resources/nodes";
import { makePolicyApi, type PolicyApi } from "./resources/policy";
@@ -17,6 +29,8 @@ import { makeUserApi, type UserApi } from "./resources/users";
import { formatServerVersion, parseServerVersion, type ServerVersion } from "./server-version";
import { createTransport } from "./transport";
const MIN_SUPPORTED_VERSION = "0.27.0";
export interface Headscale {
readonly version: ServerVersion;
readonly capabilities: Capabilities;
@@ -58,24 +72,39 @@ export async function createHeadscale(opts: CreateHeadscaleOptions): Promise<Hea
let retryTimer: ReturnType<typeof setTimeout> | undefined;
let disposed = false;
function settle(parsed: ServerVersion) {
version = parsed;
capabilities = capabilitiesFor(parsed);
detected = true;
if (parsed.unknown) {
log.warn(
"api",
"Could not parse Headscale version %s, assuming newest known capabilities",
parsed.raw,
);
} else {
log.info("api", "Connected to Headscale %s", formatServerVersion(parsed));
}
}
async function detectOnce(): Promise<boolean> {
try {
const { version: raw } = await transport.getPublic<{ version: string }>("/version");
const parsed = parseServerVersion(raw);
version = parsed;
capabilities = capabilitiesFor(parsed);
detected = true;
if (parsed.unknown) {
log.warn(
"api",
"Could not parse Headscale version %s, assuming newest known capabilities",
raw,
);
} else {
log.info("api", "Connected to Headscale %s", formatServerVersion(parsed));
}
settle(parseServerVersion(raw));
return true;
} catch (error) {
// 404 means Headscale is reachable but predates 0.27.0 (where
// /version was introduced). That server is below the supported
// floor, so we don't settle — leave capabilities permissive and
// keep retrying in case the operator upgrades in place.
if (isDataWithApiError(error) && error.data.statusCode === 404) {
log.error(
"api",
"Headscale /version returned 404; Headplane requires Headscale %s or newer",
MIN_SUPPORTED_VERSION,
);
return false;
}
log.debug("api", "Headscale /version probe failed: %s", String(error));
return false;
}
+1 -1
View File
@@ -2,7 +2,7 @@
//
// Parses the response from Headscale's `GET /version` endpoint into a
// structured value that capability checks can reason about. The
// endpoint exists in every Headscale release we support (0.26.0+),
// endpoint exists in every Headscale release we support (0.27.0+) and
// returns a plain semver-like string such as `v0.28.0`, `v0.28.0-beta.1`,
// or `dev` for untagged builds.
//